import threading

class Timer:
    """
    A thread-safe timer class that executes a callback function after a specified timeout.
    Supports starting, stopping, and modifying timeout while running.
    """
    def __init__(self):
        self._timer = None  # The underlying threading.Timer instance
        self._timeout = None  # Timeout duration in seconds
        self._callback = None  # Callback function to execute
        self._args = ()  # Positional arguments for callback
        self._kwargs = {}  # Keyword arguments for callback
        self._lock = threading.Lock()  # Thread synchronization lock
        self._is_running = False  # Flag to track timer state

    def start(self, callback, timeout, *args, **kwargs):
        """
        Start the timer with specified callback and timeout.
        
        Args:
            callback: Function to call when timer expires
            timeout: Time in seconds before callback is executed
            *args: Positional arguments to pass to callback
            **kwargs: Keyword arguments to pass to callback
        """
        with self._lock:
            # Stop any existing timer without acquiring lock again
            if self._timer:
                self._timer.cancel()
                self._timer = None
            self._is_running = False
            
            self._callback = callback
            self._timeout = timeout
            self._args = args
            self._kwargs = kwargs
            self._timer = threading.Timer(self._timeout, self._run)
            self._is_running = True
            self._timer.start()

    def _run(self):
        """Internal method called when timer expires."""
        # Set running state to False before executing callback
        with self._lock:
            self._is_running = False
            self._timer = None  # Clean up timer reference
        # Execute callback outside of lock to avoid deadlock
        if self._callback:
            try:
                self._callback(*self._args, **self._kwargs)
            except Exception:
                # If callback raises exception, it's handled silently
                # Timer is already stopped, so no additional action needed
                pass

    def stop(self):
        """Stop the timer if it's running."""
        with self._lock:
            if self._timer:
                self._timer.cancel()
                self._timer = None
            self._is_running = False

    def is_running(self):
        """
        Check if the timer is currently running.
        
        Returns:
            bool: True if timer is running, False otherwise
        """
        with self._lock:
            return self._is_running

    def set_timeout(self, timeout):
        """
        Set a new timeout value. If timer is running, it will be restarted with new timeout.
        
        Args:
            timeout: New timeout duration in seconds
        """
        with self._lock:
            self._timeout = timeout
            # If timer is running, restart with new timeout
            if self._is_running:
                # Stop existing timer without acquiring lock again
                if self._timer:
                    self._timer.cancel()
                    self._timer = None
                self._is_running = False
                
                # Start new timer with new timeout
                self._timer = threading.Timer(self._timeout, self._run)
                self._is_running = True
                self._timer.start()


class CyclicTimer(Timer):
    """
    Cyclic timer class that inherits from Timer. Once started, it runs indefinitely,
    triggering the callback function every timeout interval until manually stopped.
    """
    
    def __init__(self):
        super().__init__()
        self._is_cyclic = False  # Flag to indicate if timer is in cyclic mode
    
    def start_cyclic(self, callback, timeout, *args, **kwargs):
        """
        Start the cyclic timer that triggers callback function every timeout interval until manually stopped.
        
        Args:
            callback: Function to call at each interval
            timeout: Time in seconds between each callback execution
            *args: Positional arguments to pass to callback
            **kwargs: Keyword arguments to pass to callback
        """
        with self._lock:
            # Stop any existing timer without acquiring lock again
            if self._timer:
                self._timer.cancel()
                self._timer = None
            self._is_running = False
            self._is_cyclic = False
            
            self._callback = callback
            self._timeout = timeout
            self._args = args
            self._kwargs = kwargs
            self._is_cyclic = True
            self._timer = threading.Timer(self._timeout, self._run_cyclic)
            self._is_running = True
            self._timer.start()
    
    def _run_cyclic(self):
        """Internal method for cyclic timer execution."""
        # Execute callback function
        if self._callback:
            try:
                self._callback(*self._args, **self._kwargs)
            except Exception:
                # If callback raises exception, stop the timer
                with self._lock:
                    self._is_running = False
                    self._is_cyclic = False
                return
        
        # Restart timer if still in running state
        with self._lock:
            if self._is_running and self._is_cyclic:
                self._timer = threading.Timer(self._timeout, self._run_cyclic)
                self._timer.start()
    
    def stop(self):
        """Stop the timer (both regular and cyclic modes)."""
        with self._lock:
            if self._timer:
                self._timer.cancel()
                self._timer = None
            self._is_running = False
            self._is_cyclic = False
    
    def is_cyclic(self):
        """
        Check if the timer is in cyclic mode.
        
        Returns:
            bool: True if timer is in cyclic mode, False otherwise
        """
        with self._lock:
            return self._is_cyclic
    
    def set_timeout(self, timeout):
        """
        Set new timeout duration.
        
        Args:
            timeout: New timeout duration in seconds
            
        Note:
            For cyclic timers, the new timeout takes effect in the next cycle.
            For regular timers, the timer is restarted immediately with new timeout.
        """
        with self._lock:
            self._timeout = timeout
            # If cyclic timer is running, don't stop it - new timeout will take effect in next cycle
            if self._is_running:
                if self._is_cyclic:
                    # Cyclic timer: don't stop, new timeout will take effect in next _run_cyclic call
                    pass
                else:
                    # Regular timer: restart timer with new timeout (avoid calling super to prevent deadlock)
                    if self._timer:
                        self._timer.cancel()
                        self._timer = None
                    self._is_running = False
                    
                    # Start new timer with new timeout
                    self._timer = threading.Timer(self._timeout, self._run)
                    self._is_running = True
                    self._timer.start()

